package com.mathdroid.gui.main.character;

import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

import com.mathdroid.gui.ImageLoader;
import com.mathdroid.gui.main.DroidComboBoxModel;
import com.mathdroid.gui.main.EGlobalStrings;
import com.mathdroid.model.EModelObserverEvent;
import com.mathdroid.model.IModelObserver;
import com.mathdroid.model.Model;
import com.mathdroid.model.items.EquipedItems;
import com.mathdroid.model.items.ItemTemplates;

/**
 * This is the center of the character panel. This panel contains information
 * about the profile/character and gives the option to save and load item sets.
 * Each profile/character has its own item sets. So those templates are not
 * shared between profiles.
 */
public final class CenterContent extends JPanel implements IModelObserver {

	/**
	 * Default UID.
	 */
	private static final long serialVersionUID = 1L;
	
	/**
	 * The constraints for this panel.
	 */
	private final GridBagConstraints c = new GridBagConstraints();
	
	/**
	 * Button to save a template.
	 */
	private final JButton save = new JButton();
	
	/**
	 * Button to delete a template.
	 */
	private final JButton delete = new JButton();
	
	/**
	 * The width of the grid.
	 */
	private static final int GRID_WIDTH = 3;
	
	/**
	 * The minimum width of this panel.
	 */
	private static final int MIN_WIDTH = 350;
	
	/**
	 * The spacing between components.
	 */
	private static final int SPACING = 10;
	
	/**
	 * The width of the save/delete button.
	 */
	private static final int BUTTON_WIDTH = 25;
	
	/**
	 * The height of the save/delete button.
	 */
	private static final int BUTTON_HEIGHT = 25;
	
	/**
	 * The weight of "huge" components.
	 */
	private static final int HUGE_WEIGHT = 100;
	
	/**
	 * The tree view with informations.
	 */
	private JTree infos = null;
	
	/**
	 * The tree model root.
	 */
	private DefaultMutableTreeNode root = null;
	
	/**
	 * Combo box for templates.
	 */
	private JComboBox templates = null;
	
	/**
	 * The image loader.
	 */
	private final ImageLoader il = new ImageLoader();
	
	/**
	 * Create a new content panel for the character panel.
	 */
	public CenterContent() {
		super();
		
		Model.registerObserver(this);
		this.setLayout(new GridBagLayout());
		
		this.buildComboBox();
		this.buildSaveButton();
		this.buildDeleteButton();
		this.buildTree();

		this.setMinimumSize(new Dimension(MIN_WIDTH, 0));
	}

	/**
	 * Build the scroll pane in the center. This scroll pane contains a
	 * tree with information.
	 */
	private void buildTree() {
		this.infos = new JTree();
		this.refreshTreeModel();
		JScrollPane sp = new JScrollPane(this.infos);
		sp.setHorizontalScrollBarPolicy(
				ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
		sp.setVerticalScrollBarPolicy(
				ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
		
		this.c.fill = GridBagConstraints.BOTH;
		this.c.gridx = 0;
		this.c.gridy = 1;
		this.c.weightx = 1;
		this.c.weighty = HUGE_WEIGHT;
		this.c.gridwidth = GRID_WIDTH;
		this.c.insets = new Insets(0, 0, 0, 0);
		this.add(sp, this.c);
	}
	
	/**
	 * Source: http://www.jguru.com/faq/view.jsp?EID=513951
	 * Expands all nodes in a JTree.
	 * 
	 * @param tree
	 *            The JTree to expand.
	 * @param depth
	 *            The depth to which the tree should be expanded. Zero will just
	 *            expand the root node, a negative value will fully expand the
	 *            tree, and a positive value will recursively expand the tree to
	 *            that depth.
	 */
	public void expandJTree(final JTree tree, final int depth) {
		TreeModel model = tree.getModel();
		this.expandJTreeNode(tree, model, model.getRoot(), 0, depth);
	}

	/**
	 * Source: http://www.jguru.com/faq/view.jsp?EID=513951
	 * Expands a given node in a JTree.
	 * 
	 * @param tree
	 *            The JTree to expand.
	 * @param model
	 *            The TreeModel for tree.
	 * @param node
	 *            The node within tree to expand.
	 * @param r
	 *            The displayed row in tree that represents node.
	 * @param depth
	 *            The depth to which the tree should be expanded. Zero will just
	 *            expand node, a negative value will fully expand the tree, and
	 *            a positive value will recursively expand the tree to that
	 *            depth relative to node.
	 * @return Row.
	 */
	public int expandJTreeNode(final JTree tree, final TreeModel model, 
			final Object node, final int r, final int depth) {
		int row = r;
		
		if (node != null && !model.isLeaf(node)) {
			tree.expandRow(row);
			
			if (depth != 0) {
				for (int index = 0; row + 1 < tree.getRowCount()
						&& index < model.getChildCount(node); index++) {
					row++;
					Object child = model.getChild(node, index);
					
					if (child == null) {
						break;
					}	
					
					TreePath path = tree.getPathForRow(row);
					
					while (path != null 
							&& path.getLastPathComponent() != child) {
						row++;
						path = tree.getPathForRow(row);
					}	
					
					if (path == null) {
						break;
					}
					
					row = this.expandJTreeNode(
							tree, model, child, row, depth - 1);
				}
			}
		}
		return row;
	}
	
	/**
	 * Refresh the tree model.
	 */
	private void refreshTreeModel() {
		this.root = new DroidTreeModel();
		this.infos.setModel(new DefaultTreeModel(this.root));
		
		this.expandJTree(this.infos, 3);
	}

	/**
	 * Build the button to delete templates.
	 */
	private void buildDeleteButton() {
		this.delete.setToolTipText(EGlobalStrings.DELETE_TEMPLATE.toString());
		this.delete.setPreferredSize(
				new Dimension(BUTTON_WIDTH, BUTTON_HEIGHT));
		this.delete.setIcon(this.il.getImage("delete"));
		this.c.fill = GridBagConstraints.NONE;
		this.c.gridx = 2;
		this.c.gridy = 0;
		this.c.weightx = 1;
		this.c.weighty = 1;
		this.c.insets = new Insets(SPACING, 0, SPACING, 0);
		this.add(this.delete, this.c);	
		this.delete.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(final ActionEvent e) {
				Object o = templates.getSelectedItem();
				if (o instanceof String) {
					Model.getProfile().getItemTemplates().remove(o);
					templates.removeItem(o);
					templates.configureEditor(templates.getEditor(), null);
				}
			}
		});
	}

	/**
	 * Build the button to save a template.
	 */
	private void buildSaveButton() {
		this.save.setToolTipText(EGlobalStrings.SAVE_TEMPLATE.toString());
		this.save.setPreferredSize(
				new Dimension(BUTTON_WIDTH, BUTTON_HEIGHT));
		this.save.setIcon(this.il.getImage("add"));
		this.c.fill = GridBagConstraints.NONE;
		this.c.gridx = 1;
		this.c.gridy = 0;
		this.c.weightx = 1;
		this.c.weighty = 1;
		this.c.insets = new Insets(SPACING, 0, SPACING, SPACING);
		this.add(this.save, this.c);	
		this.save.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(final ActionEvent e) {
				saveTemplate();
				refreshTemplates();
			}
		});
	}

	/**
	 * Build the templates combo box.
	 */
	private void buildComboBox() {
		this.templates = new JComboBox(new DroidComboBoxModel());
		ItemTemplates b = Model.getProfile().getItemTemplates();
		/*
		 * Note: Adding the KEYS, not the values.
		 */
		for (String s : b.keySet()) {
			this.templates.addItem(s);
		}
		
		this.templates.setEditable(true);
		this.c.fill = GridBagConstraints.HORIZONTAL;
		this.c.gridx = 0;
		this.c.gridy = 0;
		this.c.weightx = HUGE_WEIGHT;
		this.c.weighty = 1;
		this.c.insets = new Insets(SPACING, 0, SPACING, SPACING);
		this.add(this.templates, this.c);
		
		this.templates.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(final ActionEvent e) {
				Object o = templates.getSelectedItem();
				
				if (e.getModifiers() == 16) {
					if (o instanceof String) {
						EquipedItems eq = Model.getProfile()
							.getItemTemplates().get((String) o);
						if (eq != null) {
							/*
							 * IMPORTANT: We do not want to edit the object from
							 * the templates list, so we have to clone it.
							 */
							Model.setEquipedItems((EquipedItems) eq.clone());
							delete.setEnabled(true);
						}
					}
				}
			}
		});
		
		this.templates.getEditor().getEditorComponent()
				.addKeyListener(new KeyListener() {
			@Override
			public void keyPressed(final KeyEvent e) { 
				
				if (e.getKeyCode() == KeyEvent.VK_ENTER) {
					saveTemplate();
					refreshTemplates();
				} else {
					delete.setEnabled(false);
				}
			}
			@Override
			public void keyReleased(final KeyEvent arg0) { }
			@Override
			public void keyTyped(final KeyEvent arg0) { }
		});
	}
	
	/**
	 * Save the selected template.
	 */
	private void saveTemplate() {
		Object o = templates.getEditor().getItem();
		
		if (o instanceof String) {
			final String name = o.toString();
			
			if (!name.equals("")) {
				/*
				 * Get the current equipped items. We want to save them under
				 * a new name.
				 */
				EquipedItems t = Model.getProfile().getEquipedItems();
				t.setName(name);
				
				/* Put a CLONE into the templates. */
				Model.getProfile().getItemTemplates().put(
						name, 
						(EquipedItems) t.clone());
				/*
				 * We want to use the cloned template in the profile.
				 */
				Model.setEquipedItems(t);
				
				this.delete.setEnabled(true);

				try {
					SwingUtilities.invokeLater(new Runnable() {
						public void run() {
							JOptionPane.showMessageDialog(Model.getDroidFrame(),
									"Added item template \"" + name + "\" to "
									+ "this \"" + Model.getESpec().toString() 
									+ "\" profile.\n"
									+ "Do not forget to save the profile "
									+ "or the template will be lost.",
									"Template added", 
									JOptionPane.WARNING_MESSAGE);
						}
					});
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
			/*
			 * Note that the templates are only persistent if the profile is
			 * saved!
			 */
		}
		
		/*
		 * If the item is an instance of EquipedItem, there is no need to
		 * save it, because it is saved together with the profile.
		 */
	}
	
	/**
	 * Refresh the combo box model.
	 */
	private void refreshTemplates() {
		ItemTemplates t = Model.getProfile().getItemTemplates();
		templates.removeAllItems();
		/*
		 * Add the (String) KEYS to the the comboBox.
		 */
		for (String temp : t.keySet()) {
			templates.addItem(temp);
		}
	}

	@Override
	public void modelChanged(final EModelObserverEvent e) {
		/* The profile / character overview has to be refreshed on almost
		 * every event. 
		 */
		this.refreshTreeModel();
		
		/* This is required to show the correct name in the editor. */
		try {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					templates.configureEditor(
							templates.getEditor(), Model.getProfile()
							.getEquipedItems().toString());
				}
			});
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
}
